package netTracker.network;

import java.util.*;
import java.io.*;
import java.net.*;

/**
 * Sends and receives messages.
 * Modified by David Cummings from the original source by Adam and JP.
 *
 */
public class PostOffice {
	
	private LinkedList<Message> outgoingMessages;
	private Address localAddress;
	private HashMap<Integer, Address> addressBook;
	private Thread receiveThread;
	private ReceiveWorker receiver;

	/** The default constructor. */
	public PostOffice(Address address) throws PostalException{	
		outgoingMessages = new LinkedList<Message>();
		localAddress = address;
		addressBook = new HashMap<Integer,Address>();
		
		addAddress (localAddress);

		// Set up the receiving loop:
		try{	
			receiveThread = new Thread (receiver = new ReceiveWorker (this));
			receiveThread.start ();
		}catch (IOException e){	
			System.err.println ("Error creating receiver thread: " + e); 
			throw new PostalException (localAddress);
		}
	} // PostOffice Constructor
	
	/**
	 * Adds an address to the address book.
	 * @param addr the address to add.
	 */
	public synchronized void addAddress (Address addr){
		addressBook.put (addr.getID(), addr); 
	}

	/**
	 * Returns the port of the local address.
	 * @return the port of the local address
	 */
	public int getPort(){ 
		return localAddress.getPort(); 
	}
	
	/** 
	 * Puts a message into the delivery queue.
	 * @param msg the message to enqueue
	 */
	public void enqueueMsg (Message msg){
		synchronized (outgoingMessages){
			outgoingMessages.addLast(msg);	// Add the message
			outgoingMessages.notify();		// Wake up the delivery thread
		}
	}

	/**
	 * Sends a message to a destination.
	 * @param m the message to be sent
	 * @param dest Address of Message destination
	 * @throws PostalException if message sending fails.
	 */
	public synchronized void send(Message m, Address dest) throws PostalException{	
		try{	
			/* System.out.println("Opening socket to send message: "+
				m.getMsg()+" from "+localAddress+" to "+dest); */
			Socket sock = new Socket(dest.getHostname(), dest.getPort());	// Connect to destination
			ObjectOutputStream oos = new ObjectOutputStream (				// Open output stream
				sock.getOutputStream ());
			m.setSrc (localAddress);										// Set the message source. XXX  might be unneccessary/bad!
			oos.writeObject (m);											// Send the message.
			oos.flush ();
		}catch (IOException e){	
			System.err.println ("Error sending message to "+dest+": " + e);
			throw new PostalException (dest);
		}
	}//send
	
	/**
	 * Broadcasts a message to every entry in the post office's address book.
	 * @param m the message to send.
	 */
	public synchronized void broadcast(Message m) throws PostalException{
		Collection<Address> s = addressBook.values();	// Get collection of all addresses
		Iterator<Address> itr = s.iterator();			// Iterator to iterate through addresses
		while(itr.hasNext()){
			Address next = itr.next();					// For each address:
			if(!next.equals(localAddress))				// if the address is not yourself,
				send(m, next);							// send a message to it
		}
	}
	
	/**	
	 * Receives a message.
	 * @return the received message.
	 * @throws PostalException if message receipt fails.
	 */
	public Message receive() throws InterruptedException{
		synchronized (outgoingMessages){	
			while (outgoingMessages.size () == 0) { outgoingMessages.wait (); }
			Message result = outgoingMessages.removeFirst();
			return result;
		}
	}//receive
	
	/**
	 * Looks up a target address by sending a LOOKUP request to the name server.
	 * @param addr the address to lookup
	 * @return the unique ID of the address or ERROR_ID on lookup failure or address not found
	 * @throws PostalException in case message passing fails.
	 */
	public int lookup(Address addr) throws PostalException{
		int resultID = NameServer.ERROR_ID;
		Message m = new Message("LOOKUP","",localAddress);
		m.setOtherAddr(addr);					// Set the address to lookup
		send(m, NameServer.NAMESERVER_ADDRESS);	// Send request
		try {
			m = receive();						// Receive answer from server
			resultID = m.getNumber();
		} catch (InterruptedException e) {
			System.err.println(e);
		}
		return resultID;
	}
	
	/**
	 * Enumerates the size of address book
	 * @return size of address book.
	 */
	public int enumerateAddresses(){
		return addressBook.size();
	}
	
	/**
	 * Looks up an address by ID.
	 * @param the ID to look up with
	 * @return the address with the given ID or null if it does not exist
	 */
	public synchronized Address IDtoAddress(int id){
		return addressBook.get(id); 
	}
	
	/**
	 * Queries the name server for a list of other nodes and 
	 * fills the address book with the NameServer list.
	 * @param completeTopology indicates if the address book should be complete or a ring topology.
	 */
	public void updateAddressBook() throws PostalException{
		Message m = new Message("LISTALL",localAddress);
		send(m, NameServer.NAMESERVER_ADDRESS);	// Send LISTRINGNEIGHBORS request
		try {
			m = receive();						// Receive answer from server
			addAddresses(m.getAddressBook());
		} catch (InterruptedException e) {
			System.err.println(e);
		}
	}
	
	/**
	 * Adds a set of addresses to the address book.
	 * @param newAddresses the set of addresses to add
	 */
	public void addAddresses(HashMap<Address, Integer> newAddresses){
		if (newAddresses != null){
			Set<Address> s = newAddresses.keySet();			// Get set of all addresses
			Iterator<Address> itr = s.iterator();			// Iterator to iterate through addresses
			while(itr.hasNext()){
				Address next = itr.next();					// Add all addresses to book
				addressBook.put(newAddresses.get(next), next);
			}
		}else{
			System.out.println("No addresses to add.");
		}
	}
	
	public void addHandler(String key, MsgHandler myHandler){
		receiver.addHandler(key, myHandler);
	}

	public void setLocalAddress(Address localAddress) {
		this.localAddress = localAddress;
	}
}//PostOffice Class
